package jp.fsoriented.toishi.data;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.util.Log;

/**
 * アプリケーションで保持するデータの管理を行うクラス。
 * 読み込み書き込みはスレッドセーフでないため、クラス利用者が保証しなければならない。
 * また、データ構造を変化させた場合は、stateをMODIFIEDに変更すること（変更が保存されないため）。
 */
public class AnmitsuData {

	/** シングルトン */
	private static AnmitsuData instance;
	/** JSONデータ形式のバージョン */
	private final static int CURRENT_VERSION = 1;

	private final static String LOG_TAG = "Anmitsu";
	private final static String FILE_NAME = "anmitsu.dat";

	/** データの状態 */
	public State state;
	/** 登録済みみことばデータ(not null) */
	public final List<Verse> verses = new ArrayList<Verse>();

	/**
	 * コンストラクタ
	 */
	private AnmitsuData() {
		Log.d(LOG_TAG, "[data]initialized.");
		state = State.INITIAL;
	}

	/**
	 * インスタンスを取得する。なければ生成する。
	 * 
	 * @return
	 */
	public static AnmitsuData getInstance() {
		if (instance == null) {
			instance = new AnmitsuData();
		}
		return instance;
	}

	/**
	 * データを保存する
	 * 
	 * @param c
	 */
	public void store(Context c) {
		// 変更済みでなければ、保存を行わない
		if (state != State.MODIFIED) {
			Log.w(LOG_TAG, "[data]needless to save. state:" + state);
			return;
		}
		// 保持しているデータを保存する
		try {
			JSONArray array = new JSONArray();
			for (Verse v : verses) {
				array.put(v.asJSONObject());
			}
			JSONObject obj = new JSONObject();
			obj.put("version", CURRENT_VERSION);
			obj.put("data", array);
			File file = new File(c.getFilesDir(), FILE_NAME);
			boolean succeeded = writeText(file, obj.toString());
			if (!succeeded) {
				Log.w(LOG_TAG, "[data]json store failed.");
				return;
			}
		} catch (JSONException e) {
			Log.e(LOG_TAG, "[data]FATAL json store failed.", e);
			return;
		}
		// 同期状態とする
		state = State.SYNC;
		Log.d(LOG_TAG, "[data]sync stored.");
	}

	/**
	 * データを読み込む
	 * 
	 * @param c
	 */
	public void load(Context c) {
		// 序期状態でなければ、読み込みを行わない
		if (state != State.INITIAL) {
			Log.w(LOG_TAG, "[data]needless to load. state:" + state);
			return;
		}
		// 読み込んで保持する
		File file = new File(c.getFilesDir(), FILE_NAME);
		verses.clear();
		if (file.exists()) {
			// ファイルがある場合は読み込もうとする
			String json = readText(file);
			if (json == null) {
				Log.w(LOG_TAG, "[data]json load failed.");
				return;
			}
			try {
				JSONObject jobj = new JSONObject(json);
				int version = jobj.getInt("version");
				if (version == 1) {
					JSONArray arr = jobj.getJSONArray("data");
					int count = arr.length();
					for (int i = 0; i < count; i++) {
						JSONObject obj = (JSONObject) arr.get(i);
						Verse v = Verse.fromJsonObject(obj);
						verses.add(v);
					}
				} else {
					Log.e(LOG_TAG, "[data]unknown data version: " + version);
				}
			} catch (JSONException e) {
				Log.e(LOG_TAG, "[data]FATAL json load failed.", e);
				return;
			}
		} else {
			// ファイルがない場合は読み込み済みとする
		}
		// 同期状態とする
		state = State.SYNC;
		Log.d(LOG_TAG, "[data]sync loaded");
	}

	/**
	 * データに変更を加えたことをマークします。
	 */
	public void markModified() {
		state = State.MODIFIED;
	}

	/**
	 * 文字列をファイルに保存する
	 * 
	 * @param file
	 * @param text
	 * @return
	 */
	private boolean writeText(File file, String text) {
		if (!file.exists()) {
			try {
				file.createNewFile();
			} catch (IOException e) {
			}
		}
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(file);
			fos.write(text.getBytes("UTF-8"));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			return false;
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e) {
					e.printStackTrace();
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * 文字列をファイルから読み込む
	 * 
	 * @param file
	 * @return
	 */
	private String readText(File file) {
		FileInputStream fis = null;
		byte[] buffer = new byte[1024];
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try {
			fis = new FileInputStream(file);
			int size;
			while ((size = fis.read(buffer)) != -1) {
				baos.write(buffer, 0, size);
			}
			return new String(baos.toByteArray(), "UTF-8");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}

	/**
	 * 状態を表す列挙型
	 */
	private enum State {
		INITIAL, SYNC, MODIFIED;
	}

	public static void test(Context context) {
		Log.d(LOG_TAG, "[data]###test start");
		instance = null;
		AnmitsuData data = getInstance();
		Log.d(LOG_TAG, "[data]###store should fail");
		data.store(context);
		Log.d(LOG_TAG, "[data]###load should succees even if theres no file.");
		File file = new File(context.getFilesDir(), FILE_NAME);
		file.delete();
		data.load(context);
		Log.d(LOG_TAG, "[data]###store still should fail because state is sync");
		data.store(context);
		Log.d(LOG_TAG, "[data]###store should succees coz its modified");
		Verse v = new Verse();
		v.addedDate.setTime(System.currentTimeMillis());
		v.content = "content";
		v.hint = "hint";
		v.okCount = 0;
		v.position.set(0, 0, 0);
		data.verses.add(v);
		data.markModified();
		data.store(context);
		Log.d(LOG_TAG, "[data]###load should fail because state is sync");
		data.load(context);
		instance = null;
		Log.d(LOG_TAG, "[data]###recreate...");
		data = getInstance();
		Log.d(LOG_TAG, "[data]###load will succeed");
		data.load(context);
		Log.d(LOG_TAG, "[data]###if succeeded, verses get length 1:" + data.verses.size());
		Log.d(LOG_TAG, "[data]###test end");
	}

	public static void putTestData() {
		AnmitsuData data = getInstance();
		Verse v;
		for (int i = 0; i < 10; i++) {
			v = new Verse();
			v.addedDate.setTime(System.currentTimeMillis());
			v.content = "content" + i;
			v.hint = "hint" + i;
			v.okCount = 0;
			v.position.set(0, 0, i);
			data.verses.add(v);
		}
		data.markModified();
	}

}
